Postgres建立**Postgres** Container 與 simple_bank database.
docker run --name postgres -p 5432:5432 -e POSTGRES_USER=root -e POSTGRES_PASSWORD=secret --network bank-network -d postgres:14-alpine
docker exec -it postgres createdb --username=root --owner=root simple_bank
Connect to Postgres and access simple_bankdatabase
docker exec -it postgres psql -U root simple_bank
Get current isolation level in Postgres
Postgres**預設都是Read committed
show transaction isolation level; 這個SQL指令時,所獲得的隔離級別是針對當前的console session Level**Repeatable Read**Postgres 無法修改Global Session ****isolation level
simple_bank=# show transaction isolation level;
transaction_isolation
-----------------------
read committed
(1 row)
Change isolation level in Postgres
MySQL我們在建立transactions前可以改變isolation level
Postgres 只有建立了transactions 才可以改變isolation level
-- Tx1:
simple_bank> begin;
BEGIN
simple_bank> set transaction isolation level read uncommitted;
SET
simple_bank=*# show transaction isolation level;
transaction_isolation
-----------------------
read uncommitted
(1 row)
建立accounts、entries、transfers Table
CREATE TABLE "accounts" (
"id" bigserial PRIMARY KEY,
"owner" varchar NOT NULL,
"balance" bigint NOT NULL,
"currency" varchar NOT NULL,
"created_at" timestamptz NOT NULL DEFAULT (now())
);
CREATE TABLE "entries" (
"id" bigserial PRIMARY KEY,
"account_id" bigint NOT NULL,
"amount" bigint NOT NULL,
"created_at" timestamptz NOT NULL DEFAULT (now())
);
CREATE TABLE "transfers" (
"id" bigserial PRIMARY KEY,
"from_account_id" bigint NOT NULL,
"to_account_id" bigint NOT NULL,
"amount" bigint NOT NULL,
"created_at" timestamptz NOT NULL DEFAULT (now())
);
CREATE INDEX ON "accounts" ("owner");
CREATE UNIQUE INDEX ON "accounts" ("owner", "currency");
CREATE INDEX ON "entries" ("account_id");
CREATE INDEX ON "transfers" ("from_account_id");
CREATE INDEX ON "transfers" ("to_account_id");
CREATE INDEX ON "transfers" ("from_account_id", "to_account_id");
COMMENT ON COLUMN "entries"."amount" IS 'can be negative or positive';
COMMENT ON COLUMN "transfers"."amount" IS 'must be positive';
ALTER TABLE "entries" ADD FOREIGN KEY ("account_id") REFERENCES "accounts" ("id");
ALTER TABLE "transfers" ADD FOREIGN KEY ("from_account_id") REFERENCES "accounts" ("id");
ALTER TABLE "transfers" ADD FOREIGN KEY ("to_account_id") REFERENCES "accounts" ("id");
插入三筆Account的資訊進到accounts
INSERT INTO "accounts" ("owner", "balance", "currency") VALUES
('one', 100, 'USD'),
('two', 100, 'USD'),
('three', 100, 'USD');
-- Tx1:
simple_bank=*# select * from accounts;
id | owner | balance | currency | created_at
----+-------+---------+----------+-------------------------------
1 | one | 100 | USD | 2023-08-23 11:03:23.441965+00
2 | two | 100 | USD | 2023-08-23 11:03:23.441965+00
3 | three | 100 | USD | 2023-08-23 11:03:23.441965+00
(3 rows)
Read uncommitted isolation level in Postgres建立兩個Console與Transaction,並設定兩個Session的Isolation Level為**Read Uncommitted**
-- Tx1:
simple_bank=# begin;
BEGIN
simple_bank=*# set transaction isolation level read uncommitted;
SET
simple_bank=*# show transaction isolation level;
transaction_isolation
-----------------------
read uncommitted
(1 row)
-- Tx2:
simple_bank=# begin;
BEGIN
simple_bank=*# set transaction isolation level read uncommitted;
SET
simple_bank=*# show transaction isolation level;
transaction_isolation
-----------------------
read uncommitted
(1 row)
在Tx1 中對accounts table進行select query
-- Tx1
simple_bank=*# select * from accounts;
id | owner | balance | currency | created_at
----+-------+---------+----------+-------------------------------
1 | one | 100 | USD | 2023-08-23 11:03:23.441965+00
2 | two | 100 | USD | 2023-08-23 11:03:23.441965+00
3 | three | 100 | USD | 2023-08-23 11:03:23.441965+00
(3 rows)
在Tx2 中對accounts table進行id=1的 query
-- Tx2
simple_bank=*# select * from accounts where id = 1;
id | owner | balance | currency | created_at
----+-------+---------+----------+-------------------------------
1 | one | 100 | USD | 2023-08-23 11:03:23.441965+00
(1 row)
這時候在Tx1 中對Account 1的balance減去10
-- Tx1
simple_bank=*# update accounts set balance = balance - 10 where id = 1 returning *;
id | owner | balance | currency | created_at
----+-------+---------+----------+-------------------------------
1 | one | 90 | USD | 2023-08-23 11:03:23.441965+00
(1 row)
UPDATE 1
我們在Tx2 再次對Account 1進行Query
Read uncommitted**隔離級別,但結果仍然是100美元。Read uncommitted與Read committed**行為相同。Read committed**。Read uncommitted**隔離級別並不建議使用。-- Tx2
simple_bank=*# select * from accounts where id = 1;
id | owner | balance | currency | created_at
----+-------+---------+----------+-------------------------------
1 | one | 100 | USD | 2023-08-23 11:03:23.441965+00
(1 row)
對Tx1 進行 commit 後,我們在Tx2 再次查詢Account 1,這時就可以看見Balance的修改
--Tx1
simple_bank=*# commit;
COMMIT
--Tx2
simple_bank=*# select * from accounts where id = 1;
id | owner | balance | currency | created_at
----+-------+---------+----------+-------------------------------
1 | one | 90 | USD | 2023-08-23 11:03:23.441965+00
(1 row)
simple_bank=*# commit;
COMMIT
Read committed isolation level in Postgres建立兩個Console和Transaction,並設定兩個Session的Isolation Level為**Read Committed**
-- Tx1:
simple_bank=# begin;
BEGIN
simple_bank=*# set transaction isolation level read committed;
SET
simple_bank=*# show transaction isolation level;
transaction_isolation
-----------------------
read committed
(1 row)
-- Tx2:
simple_bank=# begin;
BEGIN
simple_bank=*# set transaction isolation level read committed;
SET
simple_bank=*# show transaction isolation level;
transaction_isolation
-----------------------
read committed
(1 row)
在Tx1 中對accounts table進行select query
-- Tx1
simple_bank=*# select * from accounts;
id | owner | balance | currency | created_at
----+-------+---------+----------+-------------------------------
2 | two | 100 | USD | 2023-08-23 11:03:23.441965+00
3 | three | 100 | USD | 2023-08-23 11:03:23.441965+00
1 | one | 90 | USD | 2023-08-23 11:03:23.441965+00
(3 rows)
在Tx2 中對accounts table進行id=1的查詢並進行balance≥90的Query
dirty read現象外,還希望了解phantom read 的處理方式。-- Tx2
simple_bank=*# select * from accounts where id = 1;
id | owner | balance | currency | created_at
----+-------+---------+----------+-------------------------------
1 | one | 90 | USD | 2023-08-23 11:03:23.441965+00
(1 row)
simple_bank=*# select * from accounts where balance >= 90;
id | owner | balance | currency | created_at
----+-------+---------+----------+-------------------------------
2 | two | 100 | USD | 2023-08-23 11:03:23.441965+00
3 | three | 100 | USD | 2023-08-23 11:03:23.441965+00
1 | one | 90 | USD | 2023-08-23 11:03:23.441965+00
(3 rows)
這時候在Tx1 中對Account 1的balance減去10
-- Tx1
simple_bank=*# update accounts set balance = balance - 10 where id = 1 returning *;
id | owner | balance | currency | created_at
----+-------+---------+----------+-------------------------------
1 | one | 80 | USD | 2023-08-23 11:03:23.441965+00
(1 row)
UPDATE 1
simple_bank=*# select * from accounts where id = 1;
id | owner | balance | currency | created_at
----+-------+---------+----------+-------------------------------
1 | one | 80 | USD | 2023-08-23 11:03:23.441965+00
(1 row)
我們在Tx2 再次對Account 1進行Query
Tx1 尚未committed ,所以Tx2 的Account 1 Query 依舊是90read-committed 可以避免dirty read
-- Tx2
simple_bank=*# select * from accounts where id = 1;
id | owner | balance | currency | created_at
----+-------+---------+----------+-------------------------------
1 | one | 90 | USD | 2023-08-23 11:03:23.441965+00
(1 row)
將Tx1 進行commit 並在Tx2 再次對Account 1進行Query
Tx2 中的balance已經被更新non-repeatable
-- Tx1
simple_bank=*# commit;
COMMIT
-- Tx2
simple_bank=*# select * from accounts where id = 1;
id | owner | balance | currency | created_at
----+-------+---------+----------+-------------------------------
1 | one | 80 | USD | 2023-08-23 11:03:23.441965+00
(1 row)
我們在Tx2 再次進行balance ≥90的Query
phantom read
同樣我們在Tx2 進行同一個Query取得balance 90以上的Accounts,可以發現相同的Query但是Return 的Record 數量不相同,這就被稱為phantom read
-- Tx2
simple_bank=*# select * from accounts where balance >= 90;
id | owner | balance | currency | created_at
----+-------+---------+----------+-------------------------------
2 | two | 100 | USD | 2023-08-23 11:03:23.441965+00
3 | three | 100 | USD | 2023-08-23 11:03:23.441965+00
(2 rows)
simple_bank=*# commit;
COMMIT
所以read-committed 只能避免dirty read 無法避開non-repeatable 、phantom read
Repeatable read isolation level in Postgres建立兩個Console和Transaction,並設定兩個Session的Isolation Level為**Repeatable**
-- Tx1:
simple_bank=# begin;
BEGIN
simple_bank=*# set transaction isolation level repeatable read;
SET
simple_bank=*# show transaction isolation level;
transaction_isolation
-----------------------
repeatable read
(1 row)
-- Tx2:
simple_bank=# begin;
BEGIN
simple_bank=*# set transaction isolation level repeatable read;
SET
simple_bank=*# show transaction isolation level;
transaction_isolation
-----------------------
repeatable read
(1 row)
在Tx1 中對accounts table進行select query
-- Tx1
simple_bank=*# select * from accounts;
id | owner | balance | currency | created_at
----+-------+---------+----------+-------------------------------
2 | two | 100 | USD | 2023-08-23 11:03:23.441965+00
3 | three | 100 | USD | 2023-08-23 11:03:23.441965+00
1 | one | 80 | USD | 2023-08-23 11:03:23.441965+00
(3 rows)
在Tx2 中對accounts table進行id=1的查詢與balance ≥80 的Query
-- Tx2
simple_bank=*# select * from accounts where id = 1;
id | owner | balance | currency | created_at
----+-------+---------+----------+-------------------------------
1 | one | 80 | USD | 2023-08-23 11:03:23.441965+00
(1 row)
simple_bank=*# select * from accounts where balance >= 80;
id | owner | balance | currency | created_at
----+-------+---------+----------+-------------------------------
2 | two | 100 | USD | 2023-08-23 11:03:23.441965+00
3 | three | 100 | USD | 2023-08-23 11:03:23.441965+00
1 | one | 80 | USD | 2023-08-23 11:03:23.441965+00
(3 rows)
這時候在Tx1 中對Account 1的balance減去10
read committed」中已經阻止了dirty read , 所以就不需要再驗證-- Tx1
simple_bank=*# update accounts set balance = balance - 10 where id = 1 returning *;
id | owner | balance | currency | created_at
----+-------+---------+----------+-------------------------------
1 | one | 70 | USD | 2023-08-23 11:03:23.441965+00
(1 row)
UPDATE 1
simple_bank=*# select * from accounts;
id | owner | balance | currency | created_at
----+-------+---------+----------+-------------------------------
2 | two | 100 | USD | 2023-08-23 11:03:23.441965+00
3 | three | 100 | USD | 2023-08-23 11:03:23.441965+00
1 | one | 70 | USD | 2023-08-23 11:03:23.441965+00
(3 rows)
simple_bank=*# commit;
COMMIT
我們在Tx2 再次對Account 1進行查詢,來確認是否可以獲得Tx1 所更新的balance
Tx1已經將其更改為70並且成功commit。repeatable-read isolation level 確保所有的讀取查詢都是repeatable,這意味著,它總是返回相同的結果,即使其他已提交的交易進行了更改 (避免non-repeatable)。phantom read)。-- Tx2
simple_bank=*# select * from accounts where id = 1;
id | owner | balance | currency | created_at
----+-------+---------+----------+-------------------------------
1 | one | 80 | USD | 2023-08-23 11:03:23.441965+00
(1 row)
simple_bank=*# select * from accounts where balance >= 80;
id | owner | balance | currency | created_at
----+-------+---------+----------+-------------------------------
2 | two | 100 | USD | 2023-08-23 11:03:23.441965+00
3 | three | 100 | USD | 2023-08-23 11:03:23.441965+00
1 | one | 80 | USD | 2023-08-23 11:03:23.441965+00
(3 rows)
如果我們在Tx2 對Account 1 的balance那結果會是如何?
simple_bank=*# update accounts set balance = balance - 10 where id = 1 returning *;
ERROR: could not serialize access due to concurrent update
simple_bank=!# rollback;
ROLLBACK
建立兩個Console和Transaction,並設定兩個Session的Isolation Level為**Repeatable**
-- Tx1:
simple_bank=# begin;
BEGIN
simple_bank=*# set transaction isolation level repeatable read;
SET
simple_bank=*# show transaction isolation level;
transaction_isolation
-----------------------
repeatable read
(1 row)
-- Tx2:
simple_bank=# begin;
BEGIN
simple_bank=*# set transaction isolation level repeatable read;
SET
simple_bank=*# show transaction isolation level;
transaction_isolation
-----------------------
repeatable read
(1 row)
現在想像我們有一個使用情境,需要計算所有帳戶的餘額總和,然後用該總餘額建立一個新的帳戶
-- Tx1:
simple_bank=*# select sum(balance) from accounts;
sum
-----
270
(1 row)
總合為270美元,然後我們在帳戶表中插入一個新的記錄,其中擁有者為"sum",餘額為270,且貨幣為"USD"。
-- Tx1:
simple_bank=*# insert into accounts(owner, balance, currency) values ('sum', 270, 'USD') returning *;
id | owner | balance | currency | created_at
----+-------+---------+----------+-------------------------------
4 | sum | 270 | USD | 2023-08-23 11:46:02.890059+00
(1 row)
INSERT 0 1
simple_bank=*# select * from accounts;
id | owner | balance | currency | created_at
----+-------+---------+----------+-------------------------------
2 | two | 100 | USD | 2023-08-23 11:03:23.441965+00
3 | three | 100 | USD | 2023-08-23 11:03:23.441965+00
1 | one | 70 | USD | 2023-08-23 11:03:23.441965+00
4 | sum | 270 | USD | 2023-08-23 11:46:02.890059+00
(4 rows)
但是如果也想在Tx2 中對accounts table進行這項操作呢?
repeatable-read隔離級別,Tx2中的select查詢只會看到原始的帳戶列表,而不會看到交易1剛剛插入的新記錄。-- Tx2
simple_bank=*# select * from accounts;
id | owner | balance | currency | created_at
----+-------+---------+----------+-------------------------------
2 | two | 100 | USD | 2023-08-23 11:03:23.441965+00
3 | three | 100 | USD | 2023-08-23 11:03:23.441965+00
1 | one | 70 | USD | 2023-08-23 11:03:23.441965+00
(3 rows)
simple_bank=*# select sum(balance) from accounts;
sum
-----
270
(1 row)
simple_bank=*# insert into accounts(owner, balance, currency) values ('sum', 270, 'USD') returning *;
id | owner | balance | currency | created_at
----+-------+---------+----------+-------------------------------
4 | sum | 270 | USD | 2023-08-23 11:46:02.890059+00
(1 row)
我們再對Tx1 和Tx2進行Commit,這時Tx2 的Select會出現在duplicate sum records
Serialization anomaly**Serialization**運行的,一個接一個,那麼我們不可能有兩條總和為270的記錄。Tx1或Tx2先運行,我們應該有一條270美元的記錄,和另一條540美元的記錄。-- Tx1
simple_bank=*# commit;
COMMIT
-- Tx2
simple_bank=*# commit;
COMMIT
simple_bank=*# select * from accounts;
id | owner | balance | currency | created_at
----+-------+---------+----------+-------------------------------
2 | two | 100 | USD | 2023-08-23 11:03:23.441965+00
3 | three | 100 | USD | 2023-08-23 11:03:23.441965+00
1 | one | 70 | USD | 2023-08-23 11:03:23.441965+00
4 | sum | 270 | USD | 2023-08-23 11:46:02.890059+00
5 | sum | 270 | USD | 2023-08-23 11:46:02.890059+00
(5 rows)
所以Postgres 在repeatable-read隔離級別中無法避免**Serialization anomaly**
建立兩個Console和Transaction,並設定兩個Session的Isolation Level為Serializable
-- Tx1:
simple_bank=# begin;
BEGIN
simple_bank=*# set transaction isolation level serializable;
SET
simple_bank=*# show transaction isolation level;
transaction_isolation
-----------------------
serializable
(1 row)
-- Tx2:
simple_bank=# begin;
BEGIN
simple_bank=*# set transaction isolation level serializable;
SET
simple_bank=*# show transaction isolation level;
transaction_isolation
-----------------------
serializable
(1 row)
在Tx1 中對accounts table進行select query 並進行Sum 和Insert
-- Tx1
simple_bank=*# select * from accounts;
id | owner | balance | currency | created_at
----+-------+---------+----------+-------------------------------
2 | two | 100 | USD | 2023-08-23 11:03:23.441965+00
3 | three | 100 | USD | 2023-08-23 11:03:23.441965+00
1 | one | 70 | USD | 2023-08-23 11:03:23.441965+00
4 | sum | 270 | USD | 2023-08-23 11:46:02.890059+00
7 | sum | 270 | USD | 2023-08-23 12:15:16.939807+00
(5 rows)
simple_bank=*# select sum(balance) from accounts;
sum
-----
810
(1 row)
simple_bank=*# insert into accounts(owner, balance, currency) values ('sum2', 810, 'USD') returning *;
id | owner | balance | currency | created_at
----+-------+---------+----------+-------------------------------
10 | sum | 810 | USD | 2023-08-23 12:19:23.223866+00
(1 row)
INSERT 0 1
simple_bank=*# select * from accounts;
id | owner | balance | currency | created_at
----+-------+---------+----------+-------------------------------
2 | two | 100 | USD | 2023-08-23 11:03:23.441965+00
3 | three | 100 | USD | 2023-08-23 11:03:23.441965+00
1 | one | 70 | USD | 2023-08-23 11:03:23.441965+00
4 | sum | 270 | USD | 2023-08-23 11:46:02.890059+00
7 | sum | 270 | USD | 2023-08-23 12:15:16.939807+00
10 | sum | 810 | USD | 2023-08-23 12:19:23.223866+00
(6 rows)
在Tx2 中對accounts table進行select query 並進行Sum 和Insert
simple_bank=*# select * from accounts;
id | owner | balance | currency | created_at
----+-------+---------+----------+-------------------------------
2 | two | 100 | USD | 2023-08-23 11:03:23.441965+00
3 | three | 100 | USD | 2023-08-23 11:03:23.441965+00
1 | one | 70 | USD | 2023-08-23 11:03:23.441965+00
4 | sum | 270 | USD | 2023-08-23 11:46:02.890059+00
7 | sum | 270 | USD | 2023-08-23 12:15:16.939807+00
(5 rows)
simple_bank=*# select sum(balance) from accounts;
sum
-----
810
(1 row)
simple_bank=*# insert into accounts(owner, balance, currency) values ('sum3', 810, 'USD') returning *;
id | owner | balance | currency | created_at
----+-------+---------+----------+-------------------------------
11 | sum | 810 | USD | 2023-08-23 12:17:07.013085+00
(1 row)
INSERT 0 1
simple_bank=*# select * from accounts;
id | owner | balance | currency | created_at
----+-------+---------+----------+-------------------------------
2 | two | 100 | USD | 2023-08-23 11:03:23.441965+00
3 | three | 100 | USD | 2023-08-23 11:03:23.441965+00
1 | one | 70 | USD | 2023-08-23 11:03:23.441965+00
4 | sum | 270 | USD | 2023-08-23 11:46:02.890059+00
7 | sum | 270 | USD | 2023-08-23 12:15:16.939807+00
11 | sum | 810 | USD | 2023-08-23 12:17:07.013085+00
(6 rows)
並對Tx1 和 Tx2 進行Commit
Tx1成功提交。Tx2因交易之間的讀/寫依賴關係而拋出錯誤。concurrent的交易不再創建重複的記錄。dependencies detection機制來防止serializable anomaly。-- Tx1:
simple_bank=# commit;
COMMIT
-- Tx2:
simple_bank> commit;
ERROR: could not serialize access due to read/write dependencies among transactions
DETAIL: Reason code: Canceled on identification as a pivot, during commit attempt.
HINT: The transaction might succeed if retried.